// <copyright file="EnvironmentManager.cs" company="Selenium Committers">
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
// </copyright>

using Bazel;
using NUnit.Framework;
using OpenQA.Selenium.Internal;
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;

namespace OpenQA.Selenium.Environment;

public class EnvironmentManager
{
    private static EnvironmentManager instance;
    private readonly Type driverType;
    private IWebDriver driver;
    private readonly DriverFactory driverFactory;

    private EnvironmentManager()
    {
        string dataFilePath;
        Runfiles runfiles = null;
        try
        {
            runfiles = Runfiles.Create();
            dataFilePath = runfiles.Rlocation("_main/dotnet/test/common/appconfig.json");
        }
        catch (FileNotFoundException)
        {
            dataFilePath = "appconfig.json";
        }
        string currentDirectory = this.CurrentDirectory;

        string content = File.ReadAllText(dataFilePath);
        TestEnvironment env = JsonSerializer.Deserialize<TestEnvironment>(content, new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        });

        string activeDriverConfig = System.Environment.GetEnvironmentVariable("ACTIVE_DRIVER_CONFIG") ?? TestContext.Parameters.Get("ActiveDriverConfig", env.ActiveDriverConfig);
        string driverServiceLocation = System.Environment.GetEnvironmentVariable("DRIVER_SERVICE_LOCATION") ?? TestContext.Parameters.Get("DriverServiceLocation", env.DriverServiceLocation);

        string browserLocation = System.Environment.GetEnvironmentVariable("BROWSER_LOCATION") ?? TestContext.Parameters.Get("BrowserLocation", string.Empty);

        string activeWebsiteConfig = TestContext.Parameters.Get("ActiveWebsiteConfig", env.ActiveWebsiteConfig);
        DriverConfig driverConfig = env.DriverConfigs[activeDriverConfig];
        WebsiteConfig websiteConfig = env.WebSiteConfigs[activeWebsiteConfig];

        int port = PortUtilities.FindFreePort();
        websiteConfig.Port = port.ToString();

        TestWebServerConfig webServerConfig = env.TestWebServerConfig;
        webServerConfig.CaptureConsoleOutput = TestContext.Parameters.Get<bool>("CaptureWebServerOutput", env.TestWebServerConfig.CaptureConsoleOutput);
        webServerConfig.HideCommandPromptWindow = TestContext.Parameters.Get<bool>("HideWebServerCommandPrompt", env.TestWebServerConfig.HideCommandPromptWindow);
        webServerConfig.JavaHomeDirectory = TestContext.Parameters.Get("WebServerJavaHome", env.TestWebServerConfig.JavaHomeDirectory);
        webServerConfig.Port = websiteConfig.Port;

        this.driverFactory = new DriverFactory(driverServiceLocation, browserLocation);
        this.driverFactory.DriverStarting += OnDriverStarting;

        // Search for the driver type in the all assemblies,
        // bazel uses unpredictable assembly names to execute tests
        driverType = AppDomain.CurrentDomain.GetAssemblies()
            .Reverse()
            .Select(assembly => assembly.GetType(driverConfig.DriverTypeName))
            .FirstOrDefault(t => t != null);

        if (driverType == null)
        {
            throw new ArgumentOutOfRangeException($"Unable to find driver type {driverConfig.DriverTypeName}");
        }

        Browser = driverConfig.BrowserValue;
        RemoteCapabilities = driverConfig.RemoteCapabilities;

        UrlBuilder = new UrlBuilder(websiteConfig);

        // When run using the `bazel test` command, the following environment
        // variable will be set. If not set, we're running from a build system
        // outside Bazel, and need to locate the directory containing the jar.
        string projectRoot = System.Environment.GetEnvironmentVariable("TEST_SRCDIR");
        if (string.IsNullOrEmpty(projectRoot))
        {
            // Walk up the directory tree until we find ourselves in a directory
            // where the path to the Java web server can be determined.
            bool continueTraversal = true;
            DirectoryInfo info = new DirectoryInfo(currentDirectory);
            while (continueTraversal)
            {
                if (info == info.Root)
                {
                    break;
                }

                foreach (var childDir in info.EnumerateDirectories())
                {
                    // Case 1: The current directory of this assembly is in the
                    // same direct sub-tree as the Java targets (usually meaning
                    // executing tests from the same build system as that which
                    // builds the Java targets).
                    // If we find a child directory named "java", then the web
                    // server should be able to be found under there.
                    if (string.Compare(childDir.Name, "java", StringComparison.OrdinalIgnoreCase) == 0)
                    {
                        continueTraversal = false;
                        break;
                    }

                    // Case 2: The current directory of this assembly is a different
                    // sub-tree as the Java targets (usually meaning executing tests
                    // from a different build system as that which builds the Java
                    // targets).
                    // If we travel to a place in the tree where there is a child
                    // directory named "bazel-bin", the web server should be found
                    // in the "java" subdirectory of that directory.
                    if (string.Compare(childDir.Name, "bazel-bin", StringComparison.OrdinalIgnoreCase) == 0)
                    {
                        string javaOutDirectory = Path.Combine(childDir.FullName, "java");
                        if (Directory.Exists(javaOutDirectory))
                        {
                            info = childDir;
                            continueTraversal = false;
                            break;
                        }
                    }
                }

                if (continueTraversal)
                {
                    info = info.Parent;
                }
            }

            projectRoot = info.FullName;
        }
        else
        {
            projectRoot += "/_main";
        }

        // Find selenium-manager binary.
        try
        {
            string managerFilePath = "";
            runfiles ??= Runfiles.Create();

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                managerFilePath = runfiles.Rlocation("_main/dotnet/src/webdriver/manager/windows/selenium-manager.exe");
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                managerFilePath = runfiles.Rlocation("_main/dotnet/src/webdriver/manager/linux/selenium-manager");
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                managerFilePath = runfiles.Rlocation("_main/dotnet/src/webdriver/manager/macos/selenium-manager");
            }

            System.Environment.SetEnvironmentVariable("SE_MANAGER_PATH", managerFilePath);
        }
        catch (FileNotFoundException)
        {
            // Use the default one.
        }

        WebServer = new TestWebServer(projectRoot, webServerConfig);
        bool autoStartRemoteServer = false;
        if (Browser == Browser.Remote)
        {
            autoStartRemoteServer = driverConfig.AutoStartRemoteServer;
        }

        RemoteServer = new RemoteSeleniumServer(projectRoot, autoStartRemoteServer);
    }

    ~EnvironmentManager()
    {
        RemoteServer?.StopAsync().Wait();
        WebServer?.StopAsync().Wait();
        CloseCurrentDriver();
    }

    public event EventHandler<DriverStartingEventArgs> DriverStarting;

    public static EnvironmentManager Instance => instance ??= new EnvironmentManager();

    public Browser Browser { get; }

    public string CurrentDirectory
    {
        get
        {
            string assemblyLocation = Path.GetDirectoryName(typeof(EnvironmentManager).Assembly.Location);
            string testDirectory = TestContext.CurrentContext.TestDirectory;
            if (assemblyLocation != testDirectory)
            {
                return assemblyLocation;
            }
            return testDirectory;
        }
    }

    public TestWebServer WebServer { get; }

    public RemoteSeleniumServer RemoteServer { get; }

    public string RemoteCapabilities { get; }

    public UrlBuilder UrlBuilder { get; }

    public IWebDriver GetCurrentDriver()
    {
        return driver ?? CreateFreshDriver();
    }

    public IWebDriver CreateDriverInstance()
    {
        return driverFactory.CreateDriver(driverType);
    }

    public IWebDriver CreateDriverInstance(DriverOptions options)
    {
        return driverFactory.CreateDriverWithOptions(driverType, options);
    }

    public IWebDriver CreateFreshDriver()
    {
        CloseCurrentDriver();
        driver = CreateDriverInstance();
        return driver;
    }

    public void CloseCurrentDriver()
    {
        driver?.Quit();
        driver = null;
    }

    protected void OnDriverStarting(object sender, DriverStartingEventArgs e)
    {
        if (this.DriverStarting != null)
        {
            this.DriverStarting(sender, e);
        }
    }
}
